/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.XPath;
using System.Xml;
using DaveSexton.DocProject.Resources;
using System.IO;

namespace DaveSexton.DocProject
{
	public sealed class CommentsDocument : System.ComponentModel.ISupportInitialize
	{
		#region Public Properties
		public string FilePath
		{
			get
			{
				return filePath;
			}
		}

		public bool HasChanges
		{
			get
			{
				return hasChanges;
			}
		}

		public bool Initializing
		{
			get
			{
				return initializing;
			}
		}

		public CommentsMember this[string name]
		{
			get
			{
				EnsureDocument();

				if (!membersByName.ContainsKey(name))
				{
					XmlElement element = (XmlElement) members.SelectSingleNode("member[@name = '" + name + "']");

					if (element == null)
					{
						if (members.FirstChild == null)
							element = (XmlElement) members.AppendChild(document.CreateElement("member"));
						else
							element = (XmlElement) members.InsertBefore(document.CreateElement("member"), members.FirstChild);

						element.Attributes.Append(document.CreateAttribute("name")).Value = name;
					}

					CommentsMember member = new CommentsMember(this, element, name);

					member.SectionChanged += delegate(object sender, CommentsMemberSectionEventArgs e)
					{
						if (!initializing)
							hasChanges = true;

						OnChanged(e);
					};

					membersByName.Add(name, member);

					return member;
				}

				return membersByName[name];
			}
		}
		#endregion

		#region Private / Protected
		private bool hasChanges, loaded, create, wasCreated, initializing;
		private XmlElement members;

		private readonly Dictionary<string, CommentsMember> membersByName = new Dictionary<string, CommentsMember>(64);
		private readonly string filePath, assemblyName;
		private readonly XmlDocument document = new XmlDocument();
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="CommentsDocument" /> class.
		/// </summary>
		public CommentsDocument(string filePath)
			: this(null, filePath, true)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="CommentsDocument" /> class.
		/// </summary>
		public CommentsDocument(string assemblyName, string filePath)
			: this(assemblyName, filePath, true)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="CommentsDocument" /> class.
		/// </summary>
		/// <param name="create">Specifies whether the document should be created if the specified <paramref name="filePath"/> doesn't exist.</param>
		public CommentsDocument(string filePath, bool create)
			: this(null, filePath, create)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="CommentsDocument" /> class.
		/// </summary>
		/// <param name="create">Specifies whether the document should be created if the specified <paramref name="filePath"/> doesn't exist.</param>
		public CommentsDocument(string assemblyName, string filePath, bool create)
		{
			if (string.IsNullOrEmpty(filePath))
				throw new ArgumentException(Errors.NullOrEmpty, "filePath");

			this.filePath = filePath;
			this.assemblyName = assemblyName;
			this.create = create;

			document.PreserveWhitespace = true;
		}
		#endregion

		#region Methods
		public static void CreateProjectFile(string projectName, string path)
		{
			XmlWriterSettings settings = new XmlWriterSettings();
			settings.Indent = true;

			using (XmlWriter writer = XmlWriter.Create(path, settings))
			{
				writer.WriteStartDocument();
				writer.WriteStartElement("doc");
				writer.WriteStartElement("members");
				writer.WriteStartElement("member");
				writer.WriteStartAttribute("name");
				writer.WriteValue(projectName);
				writer.WriteEndAttribute();
				writer.WriteEndElement();
				writer.WriteEndElement();
				writer.WriteEndDocument();
			}
		}

		[System.Diagnostics.DebuggerHidden]
		private void EnsureDocument()
		{
			if (loaded)
				return;

			try
			{
				document.Load(filePath);

				XmlElement root = document.DocumentElement;

				if (root == null)
					CreateDocElement();
				else if (!root.LocalName.Equals("doc"))
					throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture, Errors.XmlCommentsFileInvalidRoot, filePath));

				if (!string.IsNullOrEmpty(assemblyName))
				{
					XmlElement assembly = root["assembly"];

					if (assembly == null)
						CreateAssemblyElement();
				}

				members = root["members"];

				if (members == null)
					CreateMembersElement();
			}
			catch (DirectoryNotFoundException)
			{
				if (create)
				{
					Directory.CreateDirectory(Path.GetDirectoryName(filePath));

					CreateDocument();
				}
				else
					throw;
			}
			catch (FileNotFoundException)
			{
				if (create)
					CreateDocument();
				else
					throw;
			}

			loaded = true;
		}

		private void CreateDocument()
		{
			document.AppendChild(document.CreateXmlDeclaration("1.0", string.Empty, string.Empty));

			CreateDocElement();
			CreateAssemblyElement();
			CreateMembersElement();

			wasCreated = true;
		}

		private void CreateDocElement()
		{
			document.AppendChild(document.CreateElement("doc"));
		}

		private void CreateAssemblyElement()
		{
			if (!string.IsNullOrEmpty(assemblyName))
			{
				XmlElement root = document.DocumentElement;
				XmlElement assembly;

				if (root.FirstChild == null)
					assembly = (XmlElement) root.AppendChild(document.CreateElement("assembly"));
				else
					assembly = (XmlElement) root.InsertBefore(document.CreateElement("assembly"), root.FirstChild);

				assembly.AppendChild(document.CreateElement("name")).InnerText = assemblyName;
			}
		}

		private void CreateMembersElement()
		{
			members = (XmlElement) document.DocumentElement.AppendChild(document.CreateElement("members"));
		}

		public void Save()
		{
			if (!loaded)
				return;

			XmlDocument clone = new XmlDocument();
			clone.LoadXml(document.OuterXml);

			DeleteEmptySections(clone);

			XmlWriterSettings settings = new XmlWriterSettings();
			settings.Indent = true;

			using (XmlWriter writer = XmlWriter.Create(filePath, settings))
			{
				writer.WriteStartDocument();
				writer.WriteNode(clone.CreateNavigator(), false);
				writer.WriteEndDocument();
			}

			hasChanges = false;

			if (wasCreated)
				OnFileCreated(EventArgs.Empty);
		}

		private void DeleteEmptySections(XmlDocument clone)
		{
			List<XPathNavigator> deleteMembers = new List<XPathNavigator>(16);

			foreach (XPathNavigator member in clone.DocumentElement["members"].CreateNavigator().SelectChildren(XPathNodeType.Element))
			{
				XPathNodeIterator sections = member.SelectChildren(XPathNodeType.Element);
				int totalCount = sections.Count;

				List<XPathNavigator> deleteSections = new List<XPathNavigator>(sections.Count);

				foreach (XPathNavigator section in sections)
				{
					if (!section.HasChildren)
					{
						if (!section.HasAttributes)
						{
							CancelCommentsMemberSectionEventArgs e = new CancelCommentsMemberSectionEventArgs(section.Name);
							OnDeleteSection(e);

							if (!e.Cancel)
								deleteSections.Add(section);
						}
						else
						{
							bool save = false;
							foreach (XPathNavigator attribute in section.Select("@*"))
							{
								if (!string.IsNullOrEmpty(attribute.Value))
								{
									save = true;
									break;
								}
							}

							if (!save)
								deleteSections.Add(section);
						}
					}
				}

				if (deleteSections.Count == totalCount)
					deleteMembers.Add(member);
				else
					foreach (XPathNavigator section in deleteSections)
						section.DeleteSelf();
			}

			foreach (XPathNavigator member in deleteMembers)
				member.DeleteSelf();
		}
		#endregion

		#region Events
		private readonly object ChangedEventLock = new object();
		private EventHandler ChangedEvent;
		private readonly object FileCreatedEventLock = new object();
		private EventHandler FileCreatedEvent;
		private readonly object DeleteSectionEventLock = new object();
		private EventHandler<CancelCommentsMemberSectionEventArgs> DeleteSectionEvent;

		/// <summary>
		/// Event raised when a member's section is being deleted.
		/// </summary>
		public event EventHandler<CancelCommentsMemberSectionEventArgs> DeleteSection
		{
			add
			{
				lock (DeleteSectionEventLock)
				{
					DeleteSectionEvent += value;
				}
			}
			remove
			{
				lock (DeleteSectionEventLock)
				{
					DeleteSectionEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="DeleteSection" /> event.
		/// </summary>
		/// <param name="e"><see cref="CancelCommentsMemberSectionEventArgs" /> object that provides the arguments for the event.</param>
		private void OnDeleteSection(CancelCommentsMemberSectionEventArgs e)
		{
			EventHandler<CancelCommentsMemberSectionEventArgs> handler = null;

			lock (DeleteSectionEventLock)
			{
				handler = DeleteSectionEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised if the <see cref="Save"/> method creates a new file.
		/// </summary>
		public event EventHandler FileCreated
		{
			add
			{
				lock (FileCreatedEventLock)
				{
					FileCreatedEvent += value;
				}
			}
			remove
			{
				lock (FileCreatedEventLock)
				{
					FileCreatedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="FileCreated" /> event.
		/// </summary>
		/// <param name="e"><see cref="EventArgs" /> object that provides the arguments for the event.</param>
		private void OnFileCreated(EventArgs e)
		{
			EventHandler handler = null;

			lock (FileCreatedEventLock)
			{
				handler = FileCreatedEvent;
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when the document has changed.
		/// </summary>
		public event EventHandler Changed
		{
			add
			{
				lock (ChangedEventLock)
				{
					ChangedEvent += value;
				}
			}
			remove
			{
				lock (ChangedEventLock)
				{
					ChangedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="Changed" /> event.
		/// </summary>
		/// <param name="e"><see cref="EventArgs" /> object that provides the arguments for the event.</param>
		private void OnChanged(EventArgs e)
		{
			EventHandler handler = null;

			lock (ChangedEventLock)
			{
				handler = ChangedEvent;
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion

		#region ISupportInitialize Members
		public void BeginInit()
		{
			initializing = true;
		}

		public void EndInit()
		{
			initializing = false;
		}
		#endregion
	}

	/// <summary>
	/// Provides arguments for the <see cref="CommentsDocument.DeleteSection"/> event.
	/// </summary>
	[Serializable]
	public class CancelCommentsMemberSectionEventArgs : CommentsMemberSectionEventArgs
	{
		#region Public Properties
		public bool Cancel { get; set; }
		#endregion

		#region Private / Protected
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="CancelCommentsMemberSectionEventArgs" /> class.
		/// </summary>
		public CancelCommentsMemberSectionEventArgs(string section)
			: base(section)
		{
		}
		#endregion
	}
}